# -*- coding: utf-8 -*-
# @Time    : 2024/6/27 14:39
# @Author  : yujiahao
# @File    : 20_pandas_multiple_index.py
# @description:Pandas分层索引

'''

分层索引（Multiple Index）是 Pandas 中非常重要的索引类型，它指的是在一个轴上拥有多个（即两个以上）索引层数，
这使得我们可以用低维度的结构来处理更高维的数据。比如，当想要处理三维及以上的高维数据时，就需要用到分层索引。

分层索引的目的是用低维度的结构（Series 或者 DataFrame）更好地处理高维数据。通过分层索引，我们可以像处理二维数据一样，处理三维及以上的数据。
分层索引的存在使得分析高维数据变得简单，让抽象的高维数据变得容易理解，同时它比废弃的 Panel 结构更容易使用。

Pandas 可以通过 MultiIndex() 方法来创建分层索引对象，该对象本质上是一个元组序列，序列中每一个元组都是唯一的。

'''
import pandas as pd
import numpy as np

# todo 1、创建分层索引
# 1) 直接创建

'''通过 MultiIndex() 的levels参数能够直接创建分层索引，示例如下：'''

# 为leves传递一个1行5列的二维数组

'''
levels参数用来创建层级索引，这里只有一层，该层的索引值分别是 np.nan, 2, NaT, None, 5；
codes表示按参数值对层级索引值排序（与 levels 中的值相对应），也就说 codes 中数值是 leves 序列的下标索引。
需要注意，这里的 -1 代表 NaN。
'''
df = pd.MultiIndex(levels=[[np.nan, 2, pd.NaT, None, 5]], codes=[[4, -1, 1, 2, 3, 4]])
print(df.levels)
print(df)

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')

# 2) 从元组创建

'''通过 from_tuples() 实现从元组创建分层索引。'''

# 创建元组序列
arrays = [['it', 'it', 'of', 'of', 'for', 'for', 'then', 'then'],
          ['one', 'two', 'one', 'two', 'one', 'two', 'one', 'two']]
# 使用zip()函数创建元组
tuples = list(zip(*arrays))
print(tuples)

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')

# 然后使用 tuples 创建分层索引，如下所示：
# 创建了两层索引，并使用names对它们命名
index = pd.MultiIndex.from_tuples(tuples, names=['first', 'second'])
print(index)

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')

# 3) 从DataFrame对象创建
'''通过 from_frame() 创建分层索引'''

df = pd.DataFrame([['bar', 'one'], ['bar', 'two'],
                   ['foo', 'one'], ['foo', 'two']],
                  columns=['first', 'second'])
# 然后使用 from_frame()创建分层索引。
index = pd.MultiIndex.from_frame(df)
# 将index应用于Series
s = pd.Series(np.random.randn(4), index=index)
print(s)

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')

# 4) 笛卡尔积创建
'''笛卡尔积（又称直积）是数学运算的一种方式，'''
# 构建数据
numbers = [0, 1, 2]
language = ['Python', 'Java']
# 经过笛卡尔积处理后会得到6中组合方式
index = pd.MultiIndex.from_product([numbers, language], names=['number', 'language'])
# 将分层索引对象应用于Series
dk_er = pd.Series(np.random.randn(6), index=index)
print(dk_er)

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')

# 5) 数组创建分层索引
'''通过 from_array() 方法，同样可以创建分层索引。'''

df = pd.MultiIndex.from_arrays([['a', 'a', 'b', 'b'], [1, 2, 1, 2]])
print(df)

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')

# todo 2、应用分层索引

# 分层索引应用到 DataFrame 中

# 创建一个数组
arrays = [[0, 0, 1, 1], ['A', 'B', 'A', 'B']]
index = pd.MultiIndex.from_arrays(arrays, names=('number', 'letter'))
# 在行索引位置应用分层索引
df = pd.DataFrame([{'a': 11, 'b': 22}], index=index)
print(df)

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')

# 通过 set_index() 可以将 DataFrame 的已有列的标索设置为 index 行索引
'''通过 set_index() 将列索引转换为了分层行索引，其中 drop=False 表示更新索引的同时，不删除 a、d 列；同时，
该函数还提供了一个 append = Ture 参数表示不添加默认的整数索引值（0到4）'''
# 创建 DataFrame
df = pd.DataFrame({
    'a': range(5),
    'b': range(5, 0, -1),
    'c': ['one', 'one', 'one', 'two', 'two'],
    'd': [0, 1, 2, 0, 1]
})

print(df)

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')

# 不追加索引列
df1 = df.set_index(['a', 'd'], drop=False)
print('不追加索引列\n', df1)

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')

# 追加索引列
df2 = df.set_index(['a', 'd'], drop=False, append=True)
print('追加索引列\n', df2)

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')

# todo 3、分层索引切片取值

# 1) 分层行索引操作
# 构建多层索引
tuple = [('湖人', 2008), ('步行者', 2008),
         ('湖人', 2007), ('凯尔特人', 2007),
         ('篮网', 2007), ('热火', 2008)]
salary = [10000, 20000, 11000, 30000, 19000, 22000]
# 其次应用于DataFrame
index = pd.MultiIndex.from_tuples(tuple)
s = pd.Series(salary, index=index)
print(s)

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')

# 切片取值
print(s['湖人', 2007])

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')

print(s['湖人'])

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')

print(s[:, 2008])

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')

# 比较value
print(s[s <= 20000])

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')

# 2) 行、列多层索引操作

'''下面看一种更加复杂的情况，就是行、列同时存在多层索引时候，应该如何通过切片取值'''

df = pd.DataFrame(np.arange(1, 13).reshape((4, 3)),
                  index=[['a', 'a', 'b', 'b'], [1, 2, 1, 2]],
                  columns=[['Jack', 'Jack', 'Helen'],
                           ['Python', 'Java', 'Python']])

print('原始数据\n', df)

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')

# 选择同一层级的索引,切记不要写成['Jack','Helen']
print('不对\n', df[['Jack', 'Helen']])

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')

# 在不同层级分别选择索引
print('在不同层级分别选择索引\n', df['Jack', 'Python'])

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')

# iloc整数索引
'''
iloc 语法
    df.iloc[行选择, 列选择]
    行选择：通过整数位置选择行。
    列选择：通过整数位置选择列。
选择行
    :3 表示选择前 3 行（即索引位置 0, 1 和 2 对应的行）。
选择列
    :2 表示选择前 2 列（即索引位置 0 和 1 对应的列）。
具体选择

在这个 DataFrame 中：
    
    行索引位置 0, 1, 2 对应的行是：
    
        ('a', 1)
        ('a', 2)
        ('b', 1)
    列索引位置 0 和 1 对应的列是：
    
        ('Jack', 'Python')
        ('Jack', 'Java')

'''
print('iloc整数索引\n', df.iloc[:3, :2])

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')

# loc列标签索引
print('iloc整数索引\n', df.loc[:, ('Helen', 'Python')])

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')

# todo 4、聚合函数应用

'''通过给level传递参数值，您可以指定在哪个层上进行聚合操作，比如求和、求均值等。示例如下：'''

df = pd.DataFrame(np.arange(1, 13).reshape((4, 3)),
                  index=[['a', 'a', 'b', 'b'], [1, 2, 1, 2]],
                  columns=[['Jack', 'Jack', 'Helen'],
                           ['Python', 'Java', 'Python']])
# 第一步，给行列层级起名字
df.index.names = ['key1', 'key2']
df.columns.names = ['name', 'course']

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')

print("原始数据\n", df)

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')

# 按 key2 级别求和

sum_by_key2 = df.groupby(level='key2').sum()
print("按 key2 级别求和\n", sum_by_key2)

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')

# 按 course 级别求平均值
'''在未来的 Pandas 版本中，使用 groupby 时指定 axis=1 的方法将被弃用。相应的替代方案是先对 DataFrame 进行转置 (.T)，然后再进行 groupby 操作。'''
# mean_by_course = df.groupby(level='course', axis=1).mean()
mean_by_course = df.T.groupby(level='course').mean().T
print("按 course 级别求平均值\n", mean_by_course)

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')

'''对于 Series 结构来说，通过给index参数传递一个二维数组就可以创建一个具有两层索引的 MultiIndex 对象'''
info = pd.Series([11, 14, 17, 24, 19, 32, 34, 27],
                 index=[['x', 'x', 'x', 'x', 'y', 'y', 'y', 'y'],
                        ['obj1', 'obj2', 'obj3', 'obj4', 'obj1', 'obj2', 'obj3', 'obj4']])
print(info)

# 可以使用 'index' 命令查看索引。
print(info.index)

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')

# todo 5、局部索引

'''局部索引可以理解为：从分层索引中选择特定索引层的一种方法。比如在下列数据中，选择所有'y'索引指定的数据，'''

info = pd.Series([11, 14, 17, 24, 19, 32, 34, 27],
                 index=[['x', 'x', 'x', 'x', 'y', 'y', 'y', 'y'],
                        ['obj1', 'obj2', 'obj3', 'obj4', 'obj1', 'obj2', 'obj3', 'obj4']])
print(info['y'])

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')

# todo 6、行索引层转换为列索引

'''
unstack() 用来将行索引转变成列索引，相当于转置操作。通过 unstack() 可以将 Series（一维序列）转变为 DataFrame（二维序列）。

unstack 是 Pandas 中用于重塑数据的方法之一，它可以将 DataFrame 或 Series 的某一层级别的索引转化为列索引。
这在处理多级索引（MultiIndex）的数据时特别有用。让我们详细讲解 unstack 的工作原理、应用场景以及一些具体的示例。

基本原理
unstack 方法的主要功能是将某个层级的行索引转换为列索引。它的基本语法是：

    DataFrame.unstack(level=-1, fill_value=None)
    Series.unstack(level=-1, fill_value=None)
    
level：指定要转换的索引层级，可以是层级的名字或整数索引。默认值为 -1，即最内层索引。
fill_value：填充值，用于填充缺失值。


    info = pd.Series([11, 14, 17, 24, 19, 32, 34, 27],
                     index=[['x', 'x', 'x', 'x', 'y', 'y', 'y', 'y'],
                            ['obj1', 'obj2', 'obj3', 'obj4', 'obj1', 'obj2', 'obj3', 'obj4']])
    # 行索引标签默认是最外层的 x, y
    # 0代表第一层索引，而1代表第二层
    print(info.unstack(0))

示例如下：
    对于 Series：
    
        unstack(level=0) 将最外层索引转换为列索引。
        unstack(level=1) 将第二层索引转换为列索引。
        unstack(level=-1) 与 level=1 效果相同，因为它表示最后一层索引。
    对于 DataFrame：
    
        unstack(level=0) 将最外层行索引转换为列索引。
        unstack(level=1) 将第二层行索引转换为列索引。
        unstack(level=-1) 与 level=1 效果相同，因为它表示最后一层行索引。

'''

# 创建一个多级索引的 DataFrame
df = pd.DataFrame(np.arange(1, 13).reshape((4, 3)),
                  index=[['a', 'a', 'b', 'b'], [1, 2, 1, 2]],
                  columns=[['Jack', 'Jack', 'Helen'], ['Python', 'Java', 'Python']])
df.index.names = ['key1', 'key2']
df.columns.names = ['name', 'course']

"""
原始 DataFrame:
name        Jack       Helen
course    Python Java Python
key1 key2                     
a    1          1    2      3
     2          4    5      6
b    1          7    8      9
     2         10   11     12

在这个 DataFrame 中：
- 行索引是 key1 和 key2 的组合。
- 列索引是 name 和 course 的组合。
- key1 和 key2 是行名称。
- name 和 course 是列名称。
"""

print("原始 DataFrame:\n")

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')

print(df)

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')

# 使用 unstack(level=0)
unstacked_df_0 = df.unstack(level=0)

"""
DataFrame 使用 unstack(level=0) 后的 DataFrame:
name     Jack               Helen    
course Python     Java     Python    
key1        a   b    a   b      a   b
key2                                 
1           1   7    2   8      3   9
2           4  10    5  11      6  12


解释:
- 行索引 key1 的值 a 和 b 被移动到列索引中。原来在行索引中的 key1 现在变成了列的一部分。
- 新的列索引：现在的列索引是一个多级索引，包括原来的列索引 name 和 course，以及新的从行索引 key1 移动过来的部分。
- 行索引 key2 仍然保持为行索引。

图示说明:
原始 DataFrame:
行索引 (key1, key2)       列索引 (name, course)
-----------------------------------------------
(a, 1)                    Jack Python    1
(a, 1)                    Jack Java      2
(a, 1)                    Helen Python   3
(a, 2)                    Jack Python    4
(a, 2)                    Jack Java      5
(a, 2)                    Helen Python   6
(b, 1)                    Jack Python    7
(b, 1)                    Jack Java      8
(b, 1)                    Helen Python   9
(b, 2)                    Jack Python   10
(b, 2)                    Jack Java     11
(b, 2)                    Helen Python  12

使用 unstack(level=0) 后:
行索引 (key2)             列索引 (name, course, key1)
-----------------------------------------------------
1                         Jack Python a      1
1                         Jack Java a        2
1                         Helen Python a     3
1                         Jack Python b      7
1                         Jack Java b        8
1                         Helen Python b     9
2                         Jack Python a      4
2                         Jack Java a        5
2                         Helen Python a     6
2                         Jack Python b     10
2                         Jack Java b       11
2                         Helen Python b    12

总结:
- 原始行索引 key1：在原始 DataFrame 中，key1 是行索引的一部分。
- 使用 unstack(level=0) 后：key1 被移动到列索引中，成为列索引的一部分。
"""

print("\nDataFrame 使用 unstack(level=0) 后的 DataFrame:\n")
print(unstacked_df_0)

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')

# todo 7、列索引实现分层

info = pd.DataFrame(np.arange(12).reshape(4, 3),
                    index=[['a', 'a', 'b', 'b'], ['one', 'two', 'three', 'four']],
                    columns=[['num1', 'num2', 'num3'], ['x', 'y', 'x']])
print(info, '\n\n', info.columns)

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')

# todo 8、交换层和层排序

# 1) 交换层
'''通过 swaplevel() 方法轻松地实现索引层交换'''

frame = pd.DataFrame(np.arange(12).reshape((4, 3)),
                     index=[['a', 'a', 'b', 'b'], [1, 2, 1, 2]],
                     columns=[['Ohio', 'Ohio', 'Colorado'],
                              ['Green', 'Red', 'Green']])
# 设置index的levels名称
frame.index.names = ['key1', 'key2']
# 设置columns的levels名称
frame.columns.names = ['state', 'color']
print('原始数据\n', frame)

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')

# 交换key1层与key层顺序
print(frame.swaplevel('key1', 'key2'))

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')

# 2) 层排序
'''通过 sort_index() 的level参数实现对层的排序。下面示例，按“key1”的字母顺序重新排序。'''

frame = pd.DataFrame(np.arange(12).reshape((4, 3)),
                     index=[['a', 'a', 'b', 'b'], [3, 2, 1, 2]],
                     columns=[['Ohio', 'Ohio', 'Colorado'],
                              ['Green', 'Red', 'Green']])
# 设置index的levels的名称，key1 与 key2分别对应不同的层
frame.index.names = ['key1', 'key2']
# 设置columns的levels的名称
frame.columns.names = ['state', 'color']

print('原始数据\n', frame)

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')

print(frame.sort_index(level='key1'))
